/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.rop.code;

import com.android.dx.util.MutabilityControl;

/**
 * Set of {@link RegisterSpec} instances, where a given register number may
 * appear only once in the set.
 */
public final class RegisterSpecSet extends MutabilityControl {

	/** {@code non-null;} no-element instance */
	public static final RegisterSpecSet EMPTY = new RegisterSpecSet(0);

	/**
	 * {@code non-null;} array of register specs, where each element is
	 * {@code null} or is an instance whose {@code reg} matches the array index
	 */
	private final RegisterSpec[] specs;

	/** {@code >= -1;} size of the set or {@code -1} if not yet calculated */
	private int size;

	/**
	 * Constructs an instance. The instance is initially empty.
	 * 
	 * @param maxSize
	 *            {@code >= 0;} the maximum register number (exclusive) that may
	 *            be represented in this instance
	 */
	public RegisterSpecSet(int maxSize) {
		super(maxSize != 0);

		this.specs = new RegisterSpec[maxSize];
		this.size = 0;
	}

	/** {@inheritDoc} */
	@Override
	public boolean equals(Object other) {
		if (!(other instanceof RegisterSpecSet)) {
			return false;
		}

		RegisterSpecSet otherSet = (RegisterSpecSet) other;
		RegisterSpec[] otherSpecs = otherSet.specs;
		int len = specs.length;

		if ((len != otherSpecs.length) || (size() != otherSet.size())) {
			return false;
		}

		for (int i = 0; i < len; i++) {
			RegisterSpec s1 = specs[i];
			RegisterSpec s2 = otherSpecs[i];

			if (s1 == s2) {
				continue;
			}

			if ((s1 == null) || !s1.equals(s2)) {
				return false;
			}
		}

		return true;
	}

	/** {@inheritDoc} */
	@Override
	public int hashCode() {
		int len = specs.length;
		int hash = 0;

		for (int i = 0; i < len; i++) {
			RegisterSpec spec = specs[i];
			int oneHash = (spec == null) ? 0 : spec.hashCode();
			hash = (hash * 31) + oneHash;
		}

		return hash;
	}

	/** {@inheritDoc} */
	@Override
	public String toString() {
		int len = specs.length;
		StringBuffer sb = new StringBuffer(len * 25);

		sb.append('{');

		boolean any = false;
		for (int i = 0; i < len; i++) {
			RegisterSpec spec = specs[i];
			if (spec != null) {
				if (any) {
					sb.append(", ");
				} else {
					any = true;
				}
				sb.append(spec);
			}
		}

		sb.append('}');
		return sb.toString();
	}

	/**
	 * Gets the maximum number of registers that may be in this instance, which
	 * is also the maximum-plus-one of register numbers that may be represented.
	 * 
	 * @return {@code >= 0;} the maximum size
	 */
	public int getMaxSize() {
		return specs.length;
	}

	/**
	 * Gets the current size of this instance.
	 * 
	 * @return {@code >= 0;} the size
	 */
	public int size() {
		int result = size;

		if (result < 0) {
			int len = specs.length;

			result = 0;
			for (int i = 0; i < len; i++) {
				if (specs[i] != null) {
					result++;
				}
			}

			size = result;
		}

		return result;
	}

	/**
	 * Gets the element with the given register number, if any.
	 * 
	 * @param reg
	 *            {@code >= 0;} the desired register number
	 * @return {@code null-ok;} the element with the given register number or
	 *         {@code null} if there is none
	 */
	public RegisterSpec get(int reg) {
		try {
			return specs[reg];
		} catch (ArrayIndexOutOfBoundsException ex) {
			// Translate the exception.
			throw new IllegalArgumentException("bogus reg");
		}
	}

	/**
	 * Gets the element with the same register number as the given spec, if any.
	 * This is just a convenient shorthand for {@code get(spec.getReg())}.
	 * 
	 * @param spec
	 *            {@code non-null;} spec with the desired register number
	 * @return {@code null-ok;} the element with the matching register number or
	 *         {@code null} if there is none
	 */
	public RegisterSpec get(RegisterSpec spec) {
		return get(spec.getReg());
	}

	/**
	 * Returns the spec in this set that's currently associated with a given
	 * local (type, name, and signature), or {@code null} if there is none. This
	 * ignores the register number of the given spec but matches on everything
	 * else.
	 * 
	 * @param spec
	 *            {@code non-null;} local to look for
	 * @return {@code null-ok;} first register found that matches, if any
	 */
	public RegisterSpec findMatchingLocal(RegisterSpec spec) {
		int length = specs.length;

		for (int reg = 0; reg < length; reg++) {
			RegisterSpec s = specs[reg];

			if (s == null) {
				continue;
			}

			if (spec.matchesVariable(s)) {
				return s;
			}
		}

		return null;
	}

	/**
	 * Returns the spec in this set that's currently associated with a given
	 * local (name and signature), or {@code null} if there is none.
	 * 
	 * @param local
	 *            {@code non-null;} local item to search for
	 * @return {@code null-ok;} first register found with matching name and
	 *         signature
	 */
	public RegisterSpec localItemToSpec(LocalItem local) {
		int length = specs.length;

		for (int reg = 0; reg < length; reg++) {
			RegisterSpec spec = specs[reg];

			if ((spec != null) && local.equals(spec.getLocalItem())) {
				return spec;
			}
		}

		return null;
	}

	/**
	 * Removes a spec from the set. Only the register number of the parameter is
	 * significant.
	 * 
	 * @param toRemove
	 *            {@code non-null;} register to remove.
	 */
	public void remove(RegisterSpec toRemove) {
		try {
			specs[toRemove.getReg()] = null;
			size = -1;
		} catch (ArrayIndexOutOfBoundsException ex) {
			// Translate the exception.
			throw new IllegalArgumentException("bogus reg");
		}
	}

	/**
	 * Puts the given spec into the set. If there is already an element in the
	 * set with the same register number, it is replaced. Additionally, if the
	 * previous element is for a category-2 register, then that previous element
	 * is nullified. Finally, if the given spec is for a category-2 register,
	 * then the immediately subsequent element is nullified.
	 * 
	 * @param spec
	 *            {@code non-null;} the register spec to put in the instance
	 */
	public void put(RegisterSpec spec) {
		throwIfImmutable();

		if (spec == null) {
			throw new NullPointerException("spec == null");
		}

		size = -1;

		try {
			int reg = spec.getReg();
			specs[reg] = spec;

			if (reg > 0) {
				int prevReg = reg - 1;
				RegisterSpec prevSpec = specs[prevReg];
				if ((prevSpec != null) && (prevSpec.getCategory() == 2)) {
					specs[prevReg] = null;
				}
			}

			if (spec.getCategory() == 2) {
				specs[reg + 1] = null;
			}
		} catch (ArrayIndexOutOfBoundsException ex) {
			// Translate the exception.
			throw new IllegalArgumentException("spec.getReg() out of range");
		}
	}

	/**
	 * Put the entire contents of the given set into this one.
	 * 
	 * @param set
	 *            {@code non-null;} the set to put into this instance
	 */
	public void putAll(RegisterSpecSet set) {
		int max = set.getMaxSize();

		for (int i = 0; i < max; i++) {
			RegisterSpec spec = set.get(i);
			if (spec != null) {
				put(spec);
			}
		}
	}

	/**
	 * Intersects this instance with the given one, modifying this instance. The
	 * intersection consists of the pairwise {@link RegisterSpec#intersect} of
	 * corresponding elements from this instance and the given one where both
	 * are non-null.
	 * 
	 * @param other
	 *            {@code non-null;} set to intersect with
	 * @param localPrimary
	 *            whether local variables are primary to the intersection; if
	 *            {@code true}, then the only non-null result elements occur
	 *            when registers being intersected have equal names (or both
	 *            have {@code null} names)
	 */
	public void intersect(RegisterSpecSet other, boolean localPrimary) {
		throwIfImmutable();

		RegisterSpec[] otherSpecs = other.specs;
		int thisLen = specs.length;
		int len = Math.min(thisLen, otherSpecs.length);

		size = -1;

		for (int i = 0; i < len; i++) {
			RegisterSpec spec = specs[i];

			if (spec == null) {
				continue;
			}

			RegisterSpec intersection = spec.intersect(otherSpecs[i],
					localPrimary);
			if (intersection != spec) {
				specs[i] = intersection;
			}
		}

		for (int i = len; i < thisLen; i++) {
			specs[i] = null;
		}
	}

	/**
	 * Returns an instance that is identical to this one, except that all
	 * register numbers are offset by the given amount. Mutability of the result
	 * is inherited from the original.
	 * 
	 * @param delta
	 *            the amount to offset the register numbers by
	 * @return {@code non-null;} an appropriately-constructed instance
	 */
	public RegisterSpecSet withOffset(int delta) {
		int len = specs.length;
		RegisterSpecSet result = new RegisterSpecSet(len + delta);

		for (int i = 0; i < len; i++) {
			RegisterSpec spec = specs[i];
			if (spec != null) {
				result.put(spec.withOffset(delta));
			}
		}

		result.size = size;

		if (isImmutable()) {
			result.setImmutable();
		}

		return result;
	}

	/**
	 * Makes and return a mutable copy of this instance.
	 * 
	 * @return {@code non-null;} the mutable copy
	 */
	public RegisterSpecSet mutableCopy() {
		int len = specs.length;
		RegisterSpecSet copy = new RegisterSpecSet(len);

		for (int i = 0; i < len; i++) {
			RegisterSpec spec = specs[i];
			if (spec != null) {
				copy.put(spec);
			}
		}

		copy.size = size;

		return copy;
	}
}
